/*
 * @(#)OgnlCallExecutor.java $version 2010. 8. 11
 *
 * Copyright 2007-2010 NHN Corp. All rights Reserved. 
 * NHN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package com.nhncorp.usf.core.ognl;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import ognl.Ognl;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import ognl.PropertyAccessor;

import org.apache.commons.collections15.map.LRUMap;

import com.nhncorp.usf.core.call.Call;
import com.nhncorp.usf.core.call.CallExecutor;
import com.nhncorp.usf.core.call.DefaultFault;
import com.nhncorp.usf.core.call.DefaultReturnValue;
import com.nhncorp.usf.core.call.Reply;
import com.nhncorp.usf.core.call.ReturnValue;
import com.nhncorp.usf.core.exception.CallExecutorException;
import com.nhncorp.usf.core.normalizer.DefaultObjectNormalizer;
import com.nhncorp.usf.core.normalizer.ObjectNormalizer;
import com.nhncorp.usf.core.normalizer.ObjectNormalizerException;
import com.nhncorp.usf.core.service.ServiceRegistry;

/**
 * Key와 value가 모두 {@link String}인 {@link Map} 하나만을 파라메터로 갖는 {@link Call}을 실행하는
 * {@link CallExecutor}. {@link Map}의 key는 메소드 파라메터의 이름으로 시작하는 <a
 * href="http://www.ognl.org/">OGNL</a> 표현식이다. 예를 들어 {@code public List&lt;Person&gt;
 * list(Condition condition)} 메소드를 호출해 이름이 '<tt>Trustin Lee</tt>'인 사람을 검색하여면 다음과
 * 같은 {@link Map} 이 필요하다. {@code condition.key=name condition.operator=equals
 * condition.value=Trustin Lee }
 * <p>
 * {@link CallExecutor}를 구현하고자 한다면 보통 이 클래스를 상속하고
 * {@link #getInvocationTarget(String)} 를 구현하면 된다. 만약, 다른 형태의
 * {@link CallExecutor}를 구현하고자 한다면, 직접 {@link CallExecutor} 인터페이스를 모두 구현해야 한다.
 * 
 * @author MiddleWare Platform Development Team
 */
public class OgnlCallExecutor implements CallExecutor { 
	private static final int EXPRESSION_CACHE_SIZE = 0xFFFF + 1;
	private static final int METHOD_CACHE_SIZE = 0xFFFF + 1;
	private static final int PARAM_TYPE_CACHE_SIZE = 0xFFFF + 1;

	private static final Logger LOG = Logger.getLogger(OgnlCallExecutor.class.getName());
	private static final Map<String, Object> EMPTY_PARAMETERS = Collections.unmodifiableMap(new HashMap<String, Object>());
	private static final DefaultInstanceFactory ARRAY_FACTORY = new DefaultArrayInstanceFactory();
	private static final Map<Class< ? >, DefaultInstanceFactory> FACTORIES = new HashMap<Class< ? >, DefaultInstanceFactory>();

	/**
	 * Gets the default instance.
	 * 
	 * @param type the type
	 * 
	 * @return the default instance
	 */
	public static Object getDefaultInstance(Class< ? > type) {
		DefaultInstanceFactory factory = getDefaultInstanceFactory(type);

		if (factory == null) {
			if (type.isArray()) {
				try {
					return ARRAY_FACTORY.getInstance(type);
				} catch (Exception e) {
					throw new InternalError();
				}
			}

			if (LOG.isLoggable(Level.FINE)) {
				LOG.fine("Using default constructor for " + type + '.');
			}

			Constructor< ? > defaultConstructor;
			try {
				defaultConstructor = type.getConstructor((Class[]) null);
			} catch (NoSuchMethodException e) {
				LOG.fine("No default constructor found for " + type + ". Using null.");
				return null;
			}

			try {
				return defaultConstructor.newInstance((Object[]) null);
			} catch (Exception e) {
				LOG.warning("Failed to create a default instance of " + type + " due to the exception raised by the default constructor. Using null.");
				return null;
			}
		}

		try {
			return factory.getInstance(type);
		} catch (Exception e) {
			LOG.warning("Failed to create a default instance of " + type + ". Using null.");
			return null;
		}
	}

	/**
	 * Gets the default instance factory.
	 * 
	 * @param type the type
	 * 
	 * @return the default instance factory
	 */
	public static DefaultInstanceFactory getDefaultInstanceFactory(Class< ? > type) {
		return FACTORIES.get(type);
	}

	/**
	 * Sets the default instance factory.
	 * 
	 * @param type the type
	 * @param factory the factory
	 */
	public static void setDefaultInstanceFactory(Class< ? > type, DefaultInstanceFactory factory) {
		if (type == null) {
			throw new NullPointerException("type");
		}

		if (factory == null) {
			throw new NullPointerException("factory");
		}

		FACTORIES.put(type, factory);
	}

	static {
		// Update property accessors.
		OgnlRuntime.setPropertyAccessor(OgnlCallParameters.class, new OgnlCallParametersPropertyAccessor());

		PropertyAccessor arrayPropertyAccessor = new OgnlArrayPropertyAccessor();
		OgnlRuntime.setPropertyAccessor(byte[].class, arrayPropertyAccessor);
		OgnlRuntime.setPropertyAccessor(short[].class, arrayPropertyAccessor);
		OgnlRuntime.setPropertyAccessor(char[].class, arrayPropertyAccessor);
		OgnlRuntime.setPropertyAccessor(int[].class, arrayPropertyAccessor);
		OgnlRuntime.setPropertyAccessor(long[].class, arrayPropertyAccessor);
		OgnlRuntime.setPropertyAccessor(float[].class, arrayPropertyAccessor);
		OgnlRuntime.setPropertyAccessor(double[].class, arrayPropertyAccessor);
		OgnlRuntime.setPropertyAccessor(Object[].class, arrayPropertyAccessor);
		OgnlRuntime.setPropertyAccessor(Object.class, new OgnlObjectPropertyAccessor());
		OgnlRuntime.setPropertyAccessor(Set.class, new OgnlSetPropertyAccessor());

		// Register default instance factories.
		setDefaultInstanceFactory(Collection.class, new DefaultListInstanceFactory());
		setDefaultInstanceFactory(List.class, new DefaultListInstanceFactory());
		setDefaultInstanceFactory(Map.class, new DefaultMapInstanceFactory());
		setDefaultInstanceFactory(Set.class, new DefaultSetInstanceFactory());
	}

	private final ObjectNormalizer normalizer = new DefaultObjectNormalizer();
	private final Map<String, Object> expressionCache = Collections.synchronizedMap(new LRUMap<String, Object>(EXPRESSION_CACHE_SIZE));
	private final Map<Class< ? >, Method[]> methodCache = Collections.synchronizedMap(new LRUMap<Class< ? >, Method[]>(METHOD_CACHE_SIZE));
	private final Map<Method, Class< ? >[]> paramTypeCache = Collections.synchronizedMap(new LRUMap<Method, Class< ? >[]>(PARAM_TYPE_CACHE_SIZE));

	/**
	 * {@inheritDoc}
	 */
	public Reply execute(ServiceRegistry serviceRegistry, Call call) {
		// Validate.
		if (call == null) {
			throw new NullPointerException("call");
		}

		try {
			call.validate();
		} catch (IllegalStateException e) {
			throw new IllegalArgumentException(SpsFaultPolicy.INVALID_CALL + call, e);
		}

		if (call.getParameters().size() > 1) {
			throw new IllegalArgumentException(SpsFaultPolicy.INVALID_PARAMETER + "Call must contain zero or one Map: " + call.getParameters());
		}

		// Get call parameters.
		Map<String, Object> requestMap = null;
		try {
			requestMap = getCallParameters(call);
		} catch (Exception e) {
			throw new IllegalArgumentException(SpsFaultPolicy.INVALID_PARAMETER + e.getMessage(), e);
		}

		// Find target object.
		Object target;
		try {
			target = serviceRegistry.getService(call.getObjectName());

			if (target == null) {
				throw new CallExecutorException(SpsFaultPolicy.INVALID_RESOURCE + "Invocation target is not found: " + call.getObjectName());
			}
		} catch (CallExecutorException e) {
			throw e;
		} catch (Exception e) {
			throw new CallExecutorException(SpsFaultPolicy.INVALID_RESOURCE + "Failed to get an invocation target: " + call.getObjectName(), e);
		}

		// Fill parameters and invoke.
		// TODO Optimize me.
		for (Method method : getMethods(target)) {
			if (!method.getName().equals(call.getMethodName())) {
				continue;
			}

			Class< ? >[] paramTypes = getParameterTypes(method);
			Object[] paramValues;

			if (paramTypes.length == 1 && paramTypes[0] == Map.class) {
				paramValues = new Object[] {requestMap};
			} else {
				OgnlCallParameters params = new OgnlCallParameters(method, paramTypes);
				Set<String> paramNames = new HashSet<String>();

				for (String name : requestMap.keySet()) {
					int delimiterPos = name.indexOf('[');

					if (delimiterPos >= 0) {
						paramNames.add(name.substring(0, delimiterPos));
						continue;
					}

					delimiterPos = name.indexOf('.');

					if (delimiterPos >= 0) {
						paramNames.add(name.substring(0, delimiterPos));
						continue;
					}

					paramNames.add(name);
				}

				if (!params.containsAllParameterNames(paramNames)) {
					LOG.fine("Found a method '" + method.getName() + "', but parameter names didn't match: " + paramNames);
					continue;
				}

				boolean mapped = true;

				for (Map.Entry<String, Object> entry : requestMap.entrySet()) {
					try {
						Object ognlExpr = parseOgnlExpression(entry.getKey());
						Ognl.setValue(ognlExpr, params, entry.getValue());
					} catch (OgnlException oe) {
						LOG.fine("Failed to set '" + entry.getKey() + "' to '" + entry.getValue() + "': " + method);
						mapped = false;
						break;
					}
				}

				if (!mapped) {
					continue;
				}

				paramValues = params.getParameterValues();
			}

			// Execute the call.
			Object returnValue;
			try {
				returnValue = method.invoke(target, paramValues);

				// It might be void if the return value is null.
				if (returnValue == null) {
					return method.getReturnType() == Void.TYPE ? ReturnValue.VOID : ReturnValue.NULL;
				} else {
					try {
						return new DefaultReturnValue(normalizer.normalize(returnValue));
					} catch (ObjectNormalizerException e) {
						throw new CallExecutorException(SpsFaultPolicy.INVALID_PARAMETER + "Failed to normalize the return value.", e);
					}
				}
			} catch (IllegalAccessException e) {
				throw new CallExecutorException(SpsFaultPolicy.INVALID_PROCEDURE + "Invocation target method is not accessible: " + call.getMethodName(), e);
			} catch (InvocationTargetException e) {
				String message = "classname=" + e.getTargetException().getClass().getName();
				return new DefaultFault(SpsFaultPolicy.getCode(e), message, normalizer.normalize(e.getTargetException()));
			}
		}

		throw new CallExecutorException(SpsFaultPolicy.INVALID_PROCEDURE + "Invocation target method is not found: " + call.getMethodName() + " on " + target.getClass().getName());
	}

	/**
	 * Gets the call parameters.
	 * 
	 * @param call the call
	 * 
	 * @return the call parameters
	 */
	@SuppressWarnings("unchecked")
	private Map<String, Object> getCallParameters(Call call) {
		if (call.getParameters().isEmpty()) {
			return EMPTY_PARAMETERS;
		} else {
			Object callParam = call.getParameters().get(0);

			if (!(callParam instanceof Map)) {
				throw new IllegalArgumentException("The parameter of the call must be a Map.");
			}

			Map< ? , ? > map = (Map< ? , ? >) callParam;

			for (Map.Entry< ? , ? > entry : map.entrySet()) {

				if (!(entry.getKey() instanceof String)) {
					throw new IllegalArgumentException("The keys of the parameter Map must be Strings.");
				}

			}

			return (Map) callParam;
		}
	}

	/**
	 * Parses the ognl expression.
	 * 
	 * @param expression the expression
	 * 
	 * @return the object
	 * 
	 * @throws OgnlException the ognl exception
	 */
	private Object parseOgnlExpression(String expression) throws OgnlException {
		Object tree = expressionCache.get(expression);

		if (tree == null) {
			tree = Ognl.parseExpression(expression);
			expressionCache.put(expression, tree);
		}

		return tree;
	}

	/**
	 * Gets the methods.
	 * 
	 * @param target the target
	 * 
	 * @return the methods
	 */
	private Method[] getMethods(Object target) {
		Class< ? > clazz = target.getClass();
		Method[] methods = methodCache.get(clazz);

		if (methods == null) {
			methods = clazz.getMethods();
			methodCache.put(clazz, methods);
		}

		return methods;
	}

	/**
	 * Gets the parameter types.
	 * 
	 * @param method the method
	 * 
	 * @return the parameter types
	 */
	private Class< ? >[] getParameterTypes(Method method) {
		Class< ? >[] paramTypes = paramTypeCache.get(method);

		if (paramTypes == null) {
			paramTypes = method.getParameterTypes();
			paramTypeCache.put(method, paramTypes);
		}

		return paramTypes;
	}
}
